package map;

import java.awt.Image;
import java.util.ArrayList;
import java.util.List;
import java.util.Observable;
import java.util.Observer;

public class Map extends Observable implements Observer
{
    protected MapData data;
    protected MapLibrary library;

    public Map(MapData data, MapLibrary library)
    {
        this.data = data;
        this.library = library;
        data.addObserver(this);
    }

    public void setMapData(MapData data)
    {
        this.data.deleteObserver(this);
        this.data = data;
        data.addObserver(this);
    }

    /**
     * Gets a properly rotated Image corresponding to the terrain tile at the
     * specified location
     * 
     * @param x
     *            X coordinate of terrain
     * @param y
     *            Y coordinate of terrain
     * @return A rotated Image corresponding to the terrain tile
     */
    public Image getTerrainImage(int x, int y)
    {
        if (data.terrain[x][y] == null
                || library.getTerrainEntry(data.terrain[x][y].getType()) == null)
            return null;
        return library.getTerrainEntry(data.terrain[x][y].getType()).getImage(
                data.terrain[x][y].getRotation());
    }

    /**
     * Gets a properly rotated Image corresponding to the item at the specified
     * location
     * 
     * @param x
     *            X coordinate of item
     * @param y
     *            Y coordinate of item
     * @return A rotated Image corresponding to the item
     */
    public Image getItemImage(int x, int y)
    {
        MapDataItemInstance item = data.getItem(x, y);
        if (item == null)
            return null;
        return library.getItemEntry(item.getType())
                .getImage(item.getRotation());
    }

    /**
     * Gets the MapLibrary currently used by this MapData
     * 
     * @return This MapData's MapLibrary
     */
    public MapLibrary getLibrary()
    {
        return library;
    }

    /* RANDOM FILL */
    private boolean invalidArray[][];

    public void randomFill()
    {
        invalidArray = new boolean[data.width][data.height];
        // The algorithm operates by placing new terrain next to existing
        // terrain. If map is completely empty to begin with, add one random
        // tile to the
        // middle.
        if (data.hasNoTerrain())
        {
            // Place a random tile
            ArrayList<MapLibraryTerrainEntry> terrainEntrys = (ArrayList<MapLibraryTerrainEntry>) library
                    .getTerrainList();
            // setTerrain((int)(Math.random() * terrain.length),
            // (int)(Math.random() * terrain[0].length),
            // terrainEntrys.get((int)(Math.random() *
            // terrainEntrys.size())).getID(),
            // (int)(Math.random() * 4));
            data.setTerrain(
                    0,
                    0,
                    terrainEntrys.get(
                            (int) (Math.random() * terrainEntrys.size()))
                            .getID(), (int) (Math.random() * 4));
        }
        while (!isFull(data.terrain))
        {
            int[] xANDy = getNonEmptySquareNextToEmptySquare();
            int x = xANDy[0];
            int y = xANDy[1];
            MapDataTerrainInstance[][] terrain = randomFillHelper(data.terrain,
                    x, y).terrain;
            System.out.println(terrain);
            if (terrain != null)
                data.setTerrain(terrain);
        }
    }

    // Fills map with tiles until is blocked in
    private RecursiveFillInfo randomFillHelper(
            MapDataTerrainInstance[][] terrain, int x, int y)
    {
        MapDataTerrainInstance[][] current = copyTerrainArray(terrain);
        ArrayList<Integer> emptyNeighbors = getEmptyNeighborDirections(current,
                x, y);
        while (emptyNeighbors.size() > 0)
        {
            // Integer direction = emptyNeighbors.remove((int)(Math.random() *
            // emptyNeighbors.size()));
            Integer direction = emptyNeighbors.remove(0);
            int nx = x, ny = y;
            switch (direction)
            {
            case 0:
                ny--;
                break;
            case 1:
                nx++;
                break;
            case 2:
                ny++;
                break;
            default:
                nx--;
                break;
            }
            if (hasNoTerrainAt(current, nx, ny))
            {
                if (invalidArray[nx][ny])
                    continue; // This configuration is
                              // illegal
                ArrayList<MapLibraryTerrainEntry> potentialTiles = (ArrayList<MapLibraryTerrainEntry>) library
                        .getPotentialAdjacentTiles(getEdges(current[x][y],
                                direction));
                search: while (true)
                {
                    if (potentialTiles.size() == 0)
                    {
                        invalidArray[x][y] = true;
                        return new RecursiveFillInfo(false, null); // This
                                                                   // configuration
                                                                   // is illegal
                    }
                    MapLibraryTerrainEntry entry = potentialTiles
                            .remove((int) (potentialTiles.size() * Math
                                    .random()));
                    int finalRotation = (int) (Math.random() * 4);
                    int rotation = finalRotation;
                    do
                    {
                        rotation = (rotation + 1) % 4;
                        current[nx][ny] = new MapDataTerrainInstance(
                                entry.getID(), rotation);
                        if (terrainTileIsLegal(current, nx, ny))
                        {
                            RecursiveFillInfo temp = randomFillHelper(current,
                                    nx, ny);
                            if (temp.wasSuccessful)
                            {
                                current = temp.terrain;
                                break search;
                            }
                        }
                    }
                    while (rotation != finalRotation);
                }
            }
        }
        if (x + 1 < data.width)
            invalidArray[x + 1][y] = false;
        if (y + 1 < data.height)
            invalidArray[x][y + 1] = false;
        if (x - 1 >= 0)
            invalidArray[x - 1][y] = false;
        if (y - 1 >= 0)
            invalidArray[x][y - 1] = false;
        return new RecursiveFillInfo(true, current);
    }

    private boolean terrainTileIsLegal(MapDataTerrainInstance[][] current,
            int nx, int ny)
    {
        int currentID = current[nx][ny].getType();
        int currentRotation = current[nx][ny].getRotation();
        MapLibraryTerrainEntry currentEntry = library
                .getTerrainEntry(currentID);
        if (hasTerrainAt(current, nx, ny - 1))
        {
            int northID = current[nx][ny - 1].getType();
            int northRotation = current[nx][ny - 1].getRotation();
            MapLibraryTerrainEntry northEntry = library
                    .getTerrainEntry(northID);
            if (!MapLibrary.edgesAreCompatible(
                    northEntry.getSouthEdges(northRotation),
                    currentEntry.getNorthEdges(currentRotation)))
                return false;
        }
        if (hasTerrainAt(current, nx, ny + 1))
        {
            int southID = current[nx][ny + 1].getType();
            int southRotation = current[nx][ny + 1].getRotation();
            MapLibraryTerrainEntry southEntry = library
                    .getTerrainEntry(southID);
            if (!MapLibrary.edgesAreCompatible(
                    southEntry.getNorthEdges(southRotation),
                    currentEntry.getSouthEdges(currentRotation)))
                return false;
        }
        if (hasTerrainAt(current, nx - 1, ny))
        {
            int westID = current[nx - 1][ny].getType();
            int westRotation = current[nx - 1][ny].getRotation();
            MapLibraryTerrainEntry westEntry = library.getTerrainEntry(westID);
            if (!MapLibrary.edgesAreCompatible(
                    westEntry.getEastEdges(westRotation),
                    currentEntry.getWestEdges(currentRotation)))
                return false;
        }
        if (hasTerrainAt(current, nx + 1, ny))
        {
            int eastID = current[nx + 1][ny].getType();
            int eastRotation = current[nx + 1][ny].getRotation();
            MapLibraryTerrainEntry eastEntry = library.getTerrainEntry(eastID);
            if (!MapLibrary.edgesAreCompatible(
                    eastEntry.getWestEdges(eastRotation),
                    currentEntry.getEastEdges(currentRotation)))
                return false;
        }
        return true;
    }

    private static boolean isFull(MapDataTerrainInstance[][] terrain)
    {
        for (int x = 0; x < terrain.length; x++)
        {
            for (int y = 0; y < terrain[x].length; y++)
            {
                if (terrain[x][y] == null)
                    return false;
            }
        }
        return true;
    }

    private MapDataTerrainInstance[][] copyTerrainArray(
            MapDataTerrainInstance[][] terrain)
    {
        MapDataTerrainInstance[][] newTerrain = new MapDataTerrainInstance[terrain.length][terrain[0].length];
        for (int x = 0; x < terrain.length; x++)
        {
            for (int y = 0; y < terrain[x].length; y++)
            {
                newTerrain[x][y] = terrain[x][y];
            }
        }
        return newTerrain;
    }

    private int[] getNonEmptySquareNextToEmptySquare()
    {
        for (int x = 0; x < data.terrain.length; x++)
        {
            for (int y = 0; y < data.terrain[x].length; y++)
            {
                if (data.hasTerrainAt(x, y)
                        && ((data.hasNoTerrainAt(x - 1, y))
                                || (data.hasNoTerrainAt(x + 1, y))
                                || (data.hasNoTerrainAt(x, y - 1)) || (data
                                    .hasNoTerrainAt(x, y + 1))))
                    return new int[] { x, y };
            }
        }
        return new int[] { -1, -1 };
    }

    private class RecursiveFillInfo
    {
        boolean wasSuccessful;
        MapDataTerrainInstance[][] terrain;

        public RecursiveFillInfo(boolean wasSuccessful,
                MapDataTerrainInstance[][] terrain)
        {
            this.wasSuccessful = wasSuccessful;
            this.terrain = terrain;
        }
    }

    private ArrayList<Integer> getEmptyNeighborDirections(
            MapDataTerrainInstance[][] terrain, int x, int y)
    {
        ArrayList<Integer> directions = new ArrayList<Integer>();
        if (hasNoTerrainAt(terrain, x - 1, y))
            directions.add(3);
        if (hasNoTerrainAt(terrain, x + 1, y))
            directions.add(1);
        if (hasNoTerrainAt(terrain, x, y - 1))
            directions.add(0);
        if (hasNoTerrainAt(terrain, x, y + 1))
            directions.add(2);
        return directions;
    }

    /**
     * Tells if location has no terrain tile This method is not the logical
     * negation of the method hasTerrainAt because both methods return false if
     * coordinates are out of bounds
     * 
     * @param x
     *            X coordinate of query location
     * @param y
     *            Y coordinate of query location
     * @return True if location has an terrain; false otherwise
     */
    private boolean hasNoTerrainAt(MapDataTerrainInstance[][] terrain, int x,
            int y)
    {
        return x >= 0 && y >= 0 && x < terrain.length && y < terrain[0].length
                && terrain[x][y] == null;
    }

    /**
     * Tells if location has a terrain tile This method is not the logical
     * negation of the method hasNoTerrainAt because both methods return false
     * if coordinates are out of bounds
     * 
     * @param x
     *            X coordinate of query location
     * @param y
     *            Y coordinate of query location
     * @return True if location has an terrain; false otherwise
     */
    private boolean hasTerrainAt(MapDataTerrainInstance[][] terrain, int x,
            int y)
    {
        return x >= 0 && y >= 0 && x < terrain.length && y < terrain[0].length
                && terrain[x][y] != null;
    }

    private List<String> getEdges(MapDataTerrainInstance terrain, int direction)
    {
        switch (direction)
        {
        case 0:
            return library.getTerrainEntry(terrain.getType()).getNorthEdges(
                    terrain.getRotation());
        case 1:
            return library.getTerrainEntry(terrain.getType()).getEastEdges(
                    terrain.getRotation());
        case 2:
            return library.getTerrainEntry(terrain.getType()).getSouthEdges(
                    terrain.getRotation());
        default:
            return library.getTerrainEntry(terrain.getType()).getWestEdges(
                    terrain.getRotation());
        }
    }

    @Override
    public void update(Observable arg0, Object arg1)
    {
        this.setChanged();
        this.notifyObservers(arg1);
    }

    public void requestRepaint()
    {
        setChanged();
        notifyObservers(MapDisplayPanel.class);
    }
}
